/**************************************************************************
 *
 * Copyright (C) 2007 University of California, Los Angeles.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 *************************************************************************/

package edu.ucla.cs.typecast.app.auction;

import java.io.*;
import java.net.*;
import java.util.*;

import edu.ucla.cs.typecast.rmi.TypeCastClient;
import edu.ucla.cs.typecast.rmi.TypeCastServer;

/**
 * A buyer in an auction.
 */

public class PlayerImpl implements Player {
	/**
	 * Info about this player.
	 */

	protected ParticipantInfo info;

	/**
	 * Auctions that this participant is aware of.
	 */

	protected SortedSet<ListingInfo> listings = new TreeSet<ListingInfo>();

	/**
	 * Items which auctions are closed and has sent "auction confirm" (as seller).
	 */

	protected Set<ItemInfo> confirmedItems = new HashSet<ItemInfo>();

	/**
	 * Items that this player has won (as buyer).
	 */

	protected Set<ItemInfo> wonItems = new HashSet<ItemInfo>();

	/**
	 * Multicast TC client used to send "auction ongoing" and "auction closed" event to buyers.
	 */

	TypeCastClient mcastTcClient;

	/**
	 * Multicast TC server used to receive "auction update" from sellers.
	 */

	TypeCastServer mcastTcServer;

	/**
	 * Unicast TC server used to receive "auction confirm" from sellers and "auction bid" from
	 * buyers.
	 */

	TypeCastServer ucastTcServer;

	/**
	 * Create a player with the given info and multicast group to communicate with the buyers.
	 * 
	 * @param info
	 * @param mcastHost
	 * @param mcastPort
	 * @throws IOException
	 */

	public PlayerImpl(ParticipantInfo info, InetAddress mcastHost, int mcastPort)
			throws IOException {
		// Store this player's info
		
		this.info = info;
		
		// Add a mcast TC client to send "auction update" events

		mcastTcClient = new TypeCastClient(mcastHost, mcastPort);

		// Add a mcast TC server to receive "auction update" events

		mcastTcServer = new TypeCastServer(mcastHost, mcastPort);
		mcastTcServer.export(this);

		// Add a ucast TC server to receive "bid" and "auction confirm" events
		
		ucastTcServer = new TypeCastServer(info.getHost(), info.getPort());
		ucastTcServer.export(this);
	}

	/**
	 * Create a seller with the given info and multicast group to communicate with the buyers.
	 * 
	 * @param info
	 * @param mcastHost
	 * @param mcastPort
	 * @throws IOException
	 */

	public PlayerImpl(ParticipantInfo info, String mcastHost, int mcastPort) throws IOException {
		this(info, InetAddress.getByName(mcastHost), mcastPort);
	}

	public void bid(ParticipantInfo sellerInfo, ItemInfo itemInfo, int price) throws IOException {
		// Publish a "bid" event to the seller
		// Create a event source on the fly

		TypeCastClient ucastTcClient = new TypeCastClient(sellerInfo.getHost(), sellerInfo
				.getPort());
		Player seller = ucastTcClient.getInvocationHandler(Player.class);
		seller.receiveAuctionBid(itemInfo, info, price);
	}

	/**
	 * Close an auction.
	 * 
	 * @param itemInfo
	 */

	public void closeAuction(ItemInfo itemInfo) {
		// Find the listing for this item

		ListingInfo listing = findListing(itemInfo);
		if (listing == null)
			return;

		// Change the end date to now

		Date now = new Date();
		listing.setEndDate(now);

		// Publish a "auction closed" event

		update(listing);
	}

	public void dispose() {
		mcastTcServer.unexport(this);
		ucastTcServer.unexport(this);
	}

	/**
	 * Find a listing that auctions a certain item.
	 * 
	 * @param itemInfo
	 * @return
	 */

	protected ListingInfo findListing(ItemInfo itemInfo) {
		// Find a listing that has the given item info

		for (ListingInfo listing : listings) {
			if (listing.getItemInfo().equals(itemInfo))
				return listing;
		}
		return null;
	}

	/**
	 * Get an auction's end time adjusted for the clock difference between buyer and seller
	 * 
	 * @param auEvent
	 * @return
	 */

	public Date getAdjustedEndTime(Date currentTime, Date endTime) {
		long timeLeft = endTime.getTime() - currentTime.getTime();
		return new Date(new Date().getTime() + timeLeft);
	}

	/**
	 * Start an auction.
	 * 
	 * @param itemInfo
	 * @param price
	 * @param endDate
	 */

	public void openAuction(ItemInfo itemInfo, int price, Date endDate) {
		// Add this auction to the auction list

		ListingInfo listing = new ListingInfo(itemInfo, price, endDate, info);
		listings.add(listing);

		// Send a "auction update" event
		
		update(listing);
	}

	public void receiveAuctionBid(ItemInfo itemInfo, ParticipantInfo buyerInfo, int price) {
		// See if we can find this auction listing
		// Unless we can find it and it is ongoing, do nothing

		ListingInfo listing = findListing(itemInfo);
		if (listing == null)
			return;

		if (!listing.getEndDate().after(new Date()))
			return;

		// Decide whether to accept this bid
		// Accept if the bidding price is higher than the previous high

		if (price > listing.getPrice()) {
			// Accept by updating this listing's highest bidder and price

			listing.setHighestBidderInfo(buyerInfo);
			listing.setPrice(price);

			// Send a "auction ongoing" event

			update(listing);
		}
	}

	public void receiveAuctionConfirm(ItemInfo itemInfo) {
		// We have won an item!
		// See if this message is a duplicate
		// It is not a dupe if the item is not in the "items won" list

		if (!wonItems.contains(itemInfo)) {
			// Not a dupe!
			// Add item to "items won" list

			wonItems.add(itemInfo);
		}
	}

	public void receiveAuctionUpdate(ItemInfo itemInfo, int price, Date currentTime, Date endTime,
			ParticipantInfo sellerInfo, ParticipantInfo highestBidderInfo) {
		// See if the auction is already in our list
		// If so, update that auction entry withe the given bidder and price

		boolean found = false;
		for (ListingInfo listing : listings) {
			if (listing.getItemInfo().equals(itemInfo)) {
				listing.setHighestBidderInfo(highestBidderInfo);
				listing.setPrice(price);
				listing.setEndDate(endTime);
				found = true;
			}
		}

		// If not, add the auction to our list

		if (!found) {
			listings.add(new ListingInfo(itemInfo, price, getAdjustedEndTime(currentTime, endTime),
					sellerInfo));
		}
	}

	/**
	 * Send out an (periodical) update about the auctions.
	 */

	public void update() {
		try {
			for (ListingInfo listing : listings) {
				update2(listing);
			}
		} catch (IOException e) {
			// Print error message
			e.printStackTrace();
		}
	}

	/**
	 * Send out an (periodical) update about a particular auction
	 */

	public void update(ListingInfo listing) {
		try {
			update2(listing);
		} catch (IOException e) {
			// Print error message
			e.printStackTrace();
		}
	}

	/**
	 * Send out an (periodical) update about a particular auction
	 */

	protected void update2(ListingInfo listing) throws IOException {
		Date now = new Date();

		// Unless, this auction in our list,
		// do nothing

		if (!listings.contains(listing))
			return;

		// Unless auction is closed and "auction confirm" sent,
		// send "auction update" event

		boolean auctionOpen = now.before(listing.getEndDate());
		boolean confirmSent = false;
		try {
			confirmSent = confirmedItems.contains(listing.getItemInfo());
		} catch (Exception e) {
			System.out.println("Exception here, getItemInfo() returns: " + listing.getItemInfo());
			e.printStackTrace();
		}

		if (!(auctionOpen && confirmSent)) {
			Player player = mcastTcClient.getInvocationHandler(Player.class);
			player.receiveAuctionUpdate(listing.getItemInfo(), listing.getPrice(), now, listing
					.getEndDate(), info, listing.getHighestBidderInfo());
		}

		// If auction is closed but "auction confirm" is not sent,
		// send "auction confirm" (if anyone has bidded) and mark auction as
		// confirmed

		if (!auctionOpen && !confirmSent) {
			// If anyone has bidded on it,
			// send "auction confirm" event to highest bidder

			ParticipantInfo highestBidder = listing.getHighestBidderInfo();
			if (highestBidder != null) {
				TypeCastClient ucastTcClient = new TypeCastClient(highestBidder.getHost(),
						highestBidder.getPort());
				Player buyer = ucastTcClient.getInvocationHandler(Player.class);
				buyer.receiveAuctionConfirm(listing.getItemInfo());
			}

			// Mark auction as confirmed

			confirmedItems.add(listing.getItemInfo());
		}
	}

	public Set<ItemInfo> getConfirmedItems() {
		return confirmedItems;
	}

	public Set<ItemInfo> getWonItems() {
		return wonItems;
	}

	public SortedSet<ListingInfo> getAuctionInfoList() {
		return listings;
	}

	public ParticipantInfo getInfo() {
		return info;
	}
}
